package com.googlecode.wmlynar.gwtbeans.gen;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.BeanProperty;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.Type;

/**
 * Class generating the BindingFactory.
 * 
 * @author Wojciech Mlynarczyk (woj)
 * 
 */
public class BindingFacoryGenerator {

    private final Map<PropertyAccessor, String> propertyAccessors = new HashMap<PropertyAccessor, String>();
    private int id = 0;

    JavaDocBuilder builder;
    Mappings mappings;

    public BindingFacoryGenerator(final JavaDocBuilder builder) {
        this.builder = builder;
    }

    public String generate(final Mappings mappings, final String packageName,
            final String factoryName) {

        this.mappings = mappings;

        for (final Mapping mapping : mappings.getMappingList()) {
            checkIfAutomapping(mapping);
        }

        augment(mappings);

        return generateClass(mappings, packageName, factoryName);
    }

    private void checkIfAutomapping(final Mapping mapping) {
        if (!mapping.isAutoMapping()) {
            return;
        }
        if (!mapping.getFields().isEmpty()) {
            throw new RuntimeException("Do not specify any fields when automappting");
        }
        if (!mapping.getClassAName().equals(mapping.getClassBName())) {
            throw new RuntimeException("Class names need to be the same when automapping");
        }

        final ArrayList<Field> fields = new ArrayList<Field>();

        final BeanProperty[] properties = builder.getClassByName(mapping.getClassAName())
                .getBeanProperties();

        final int level = 0;
        for (final BeanProperty p : properties) {
            addFields(fields, level, "", p);
        }

        mapping.setFields(fields);
    }

    private void addFields(final ArrayList<Field> fields, int level, String name,
            final BeanProperty p) {
        level++;
        if (level > 100) {
            throw new RuntimeException("Maximum recursion level reached: " + name);
        }
        name = name + p.getName();
        final BeanProperty[] properties = p.getType().getJavaClass().getBeanProperties();
        if (p.getType().isPrimitive() || p.getType().isArray() || properties.length == 0
                || !recurseAutomap(p)) {
            if (performAutomap(p)) {
                final Field f = new Field();
                f.setFieldAName(name);
                f.setFieldBName(name);
                fields.add(f);
            } else {
                System.out.println("skipping automap for " + p.getName());
            }
        } else {
            name = name + ".";
            for (final BeanProperty p2 : properties) {
                addFields(fields, level, name, p2);
            }
        }
    }

    private boolean recurseAutomap(final BeanProperty p) {
        // do not deep copy system classes
        final String name = p.getType().getValue();
        if (name.startsWith("java.") || name.startsWith("javax.")) {
            System.out.println("no deep copy of system classes " + name);
            return false;
        }
        if (mappings.getImmutableList().contains(name)) {
            System.out.println("no deep copy immutable class " + name);
            return false;
        }
        return true;
    }

    private boolean performAutomap(final BeanProperty p) {
        // do not deep copy when there is no mutator or no accessor
        if (p.getAccessor() == null || p.getMutator() == null) {
            System.out.println("no mutator or accessor for property " + p.getName());
            return false;
        }
        return true;
    }

    /**
     * This method is public for easy testing.
     */
    public void augment(final Mappings mappings) {
        for (final Mapping mapping : mappings.getMappingList()) {
            augument(mapping);
        }
        for (final Binding binding : mappings.getBindingList()) {
            augument(binding);
        }
    }

    private void augument(final Mapping binding) {
        binding.setClassA(builder.getClassByName(binding.getClassAName()));
        binding.setClassB(builder.getClassByName(binding.getClassBName()));

        for (final Field field : binding.getFields()) {
            field.setPropertyListA(getPropertyList(binding.getClassA(), field.getFieldAName()));
            field.setPropertyListB(getPropertyList(binding.getClassB(), field.getFieldBName()));
        }
    }

    private ArrayList<BeanProperty> getPropertyList(JavaClass cl, final String fieldName) {
        final ArrayList<BeanProperty> list = new ArrayList<BeanProperty>();
        final StringTokenizer tokenizer1 = new StringTokenizer(fieldName, ".");
        while (tokenizer1.hasMoreElements()) {
            final String e = tokenizer1.nextToken();
            final BeanProperty p = cl.getBeanProperty(e, true);
            if (p == null) {
                throw new RuntimeException("cannot find property " + e + " in " + cl.getName()
                        + ", " + fieldName);
            }
            list.add(p);
            // get next class in chain
            cl = p.getType().getJavaClass();
        }
        return list;
    }

    private String generateClass(final Mappings mappings, final String packageName,
            final String className) {
        final List<Binding> bindingList = mappings.getBindingList();
        final List<Mapping> mappingList = mappings.getMappingList();

        final StringBuilder sb = new StringBuilder();
        sb.append("package " + packageName + ";\n\n"
                + "import com.googlecode.wmlynar.gwtbeans.client.beans.Property;\n"
                + "import com.googlecode.wmlynar.gwtbeans.client.beans.Binding;\n"
                + "import com.googlecode.wmlynar.gwtbeans.client.beans.BindingGroup;\n\n");

        sb.append("public class " + className + " {\n");

        // generate copy functions

        for (final Mapping mapping : mappingList) {
            sb.append("\n");
            sb.append(generateCopyFunctions(mapping));
        }

        // generate binding functions

        for (final Binding binding : bindingList) {
            sb.append("\n");
            sb.append(generateBindingGroup(binding));
        }

        // generate properties

        for (final PropertyAccessor accessor : propertyAccessors.keySet()) {
            sb.append("\n");
            sb.append(generatePropertyAccessor(propertyAccessors.get(accessor), accessor));
        }

        sb.append("\n}\n");
        return sb.toString();
    }

    /**
     * This function generates this:
     * 
     * <pre>
     * public static com.googlecode.wmlynar.gwtbeans.gen.Bean1 copy(
     *         final com.googlecode.wmlynar.gwtbeans.gen.Bean2 in,
     *         final com.googlecode.wmlynar.gwtbeans.gen.Bean1 out) {
     *     if (out.getBean1() == null) {
     *         out.setBean1(new com.googlecode.wmlynar.gwtbeans.gen.Bean1());
     *     }
     *     out.getBean1().setString1(in.getString2());
     *     return out;
     * }
     * </pre>
     * 
     * And copy in other direction
     * 
     */
    private String generateCopyFunctions(final Mapping mapping) {
        final StringBuilder sb = new StringBuilder();

        // copy in one direction

        sb.append("  public static " + mapping.getClassBName() + " copy(final "
                + mapping.getClassAName() + " in, final " + mapping.getClassBName() + " out) {\n");

        for (final Field field : mapping.getFields()) {
            final ArrayList<BeanProperty> plist = field.getPropertyListB();
            final ArrayList<BeanProperty> plist2 = field.getPropertyListA();

            if (mapping.getClassAName().equals(mapping.getClassBName())) {
                // TODO: THINK ABOUT THIS: maybe some more conditions, isAssignableFrom?
                if (!convertersEqual(field.getToAConverterName(), field.getToBConverterName())) {
                    throw new RuntimeException(
                            "Identical classes should have the same converters for each filed");
                }
            }

            generateCopyField(sb, plist, plist2, field.getToBConverterName());
        }

        sb.append("    return out;\n");
        sb.append("  }\n");

        if (mapping.getClassAName().equals(mapping.getClassBName())) {
            return sb.toString();
        }

        // if classes A and B are different then generate symmetric copy function

        sb.append("\n");
        sb.append("  public static " + mapping.getClassAName() + " copy(final "
                + mapping.getClassBName() + " in, final " + mapping.getClassAName() + " out) {\n");

        for (final Field field : mapping.getFields()) {
            final ArrayList<BeanProperty> plist = field.getPropertyListA();
            final ArrayList<BeanProperty> plist2 = field.getPropertyListB();

            generateCopyField(sb, plist, plist2, field.getToAConverterName());
        }

        sb.append("    return out;\n");
        sb.append("  }\n");

        return sb.toString();
    }

    private boolean convertersEqual(final String a, final String b) {
        if (a == null && b == null) {
            return true;
        }
        if (a == null || b == null) {
            return false;
        }
        return a.equals(b);
    }

    /**
     * This function generates this:
     * 
     * <pre>
     *     if (out.getBean1() == null) {
     *         out.setBean1(new com.googlecode.wmlynar.gwtbeans.gen.Bean1());
     *     }
     *     out.getBean1().setString1(in.getString2());
     * }
     * </pre>
     * 
     */
    private void generateCopyField(final StringBuilder sb, final ArrayList<BeanProperty> plist,
            final ArrayList<BeanProperty> plist2, final String converterName) {

        String accessorName = "out";
        String mutatorName;
        for (int i = 0; i < plist.size() - 1; i++) {
            final BeanProperty p = plist.get(i);

            final String type = p.getType().getValue();
            //checkMutator(p);
            checkAccessor(p);
            if (p.getMutator() == null) {
                mutatorName = null;
            } else {
                mutatorName = accessorName + "." + p.getMutator().getName();
            }
            accessorName = accessorName + "." + p.getAccessor().getName() + "()";

            // make sure field is not null, if it is null create a bean

            // if there is no mutator assume it is always not null 
            if (mutatorName != null) {
                sb.append("    if (" + accessorName + " == null) {\n");
                sb.append("      " + mutatorName + "(new " + type + "());\n");
                sb.append("    }\n");
            }
        }

        checkMutator(plist.get(plist.size() - 1));

        // get the mutator of the last one in chain
        mutatorName = accessorName + "." + plist.get(plist.size() - 1).getMutator().getName();
        // get the accessor of the other property
        String accessor2Name = "in";
        for (final BeanProperty p2 : plist2) {
            checkAccessor(p2);
            accessor2Name = accessor2Name + "." + p2.getAccessor().getName() + "()";
        }

        // list should be copied
        if (isList(plist2.get(plist2.size() - 1))) {
            final BeanProperty p = plist.get(plist.size() - 1);
            checkAccessor(p);
            accessorName = accessorName + "." + p.getAccessor().getName() + "()";
            sb.append("    " + mutatorName + "(new java.util.ArrayList());\n");
            sb.append("    java.util.Collections.copy(" + accessor2Name + "," + accessorName
                    + ");\n");
            return;
        }

        // array should be cloned
        if (isCloneable(plist2.get(plist2.size() - 1))) {
            accessor2Name = accessor2Name + ".clone()";
        }

        // TODO: what to do generally with java.util.Collection ????

        if (converterName != null) {
            accessor2Name = converterName + "(" + accessor2Name + ")";
        }

        sb.append("    " + mutatorName + "(" + accessor2Name + ");\n");
    }

    private void checkMutator(final BeanProperty p) {
        if (p.getMutator() == null) {
            throw new RuntimeException("No mutator for property: " + p.getName());
        }
    }

    private void checkAccessor(final BeanProperty p) {
        if (p.getAccessor() == null) {
            throw new RuntimeException("No accessor for property: " + p.getName());
        }
    }

    private boolean isCloneable(final BeanProperty beanProperty) {
        // TODO: improve this. lack of time, you know...
        final Type t = beanProperty.getType();
        if (t.isArray()) {
            return true;
        }
        return false;
    }

    public boolean isList(final BeanProperty beanProperty) {
        final Type t = beanProperty.getType();
        //if (t.getJavaClass().isA("java.util.Collection")) {
        if (t.getJavaClass().isA("java.util.List")) {
            return true;
        }
        return false;
    }

    private String generateBindingGroup(final Binding binding) {
        final StringBuilder sb = new StringBuilder();
        sb.append("public static BindingGroup createBinding(final " + binding.getClassAName()
                + " object1, final " + binding.getClassBName()
                + " object2, final String bindingName) {\n"
                + "  final BindingGroup g = new BindingGroup();\n");

        for (final Field field : binding.getFields()) {
            String className = binding.getClassAName();
            sb.append("  g.add(new Binding(object1, new Property[] { ");
            for (final BeanProperty property : field.getPropertyListA()) {
                sb.append(getPropertyAccessorName(className, property));
                sb.append(", ");
                className = property.getType().getGenericValue();
            }
            className = binding.getClassBName();
            sb.append(" }, ");
            if (field.getToAConverterName() == null) {
                sb.append("null");
            } else {
                sb.append(field.getToAConverterName() + ".INSTANCE");
            }
            sb.append(", object2, new Property[] { ");
            for (final BeanProperty property : field.getPropertyListB()) {
                sb.append(getPropertyAccessorName(className, property));
                sb.append(", ");
                className = property.getType().getGenericValue();
            }
            sb.append("}, ");
            if (field.getToBConverterName() == null) {
                sb.append("null");
            } else {
                sb.append(field.getToBConverterName() + ".INSTANCE");
            }
            sb.append("));\n");
        }

        sb.append("  return g;\n}\n");
        return sb.toString();
    }

    private String getBoxedClassName(final String name) {
        if ("boolean".equals(name)) {
            return "Boolean";
        } else if ("char".equals(name)) {
            return "Character";
        } else if ("byte".equals(name)) {
            return "Byte";
        } else if ("short".equals(name)) {
            return "Short";
        } else if ("int".equals(name)) {
            return "Integer";
        } else if ("long".equals(name)) {
            return "Long";
        } else if ("float".equals(name)) {
            return "Float";
        } else if ("double".equals(name)) {
            return "Double";
        }
        return name;
    }

    private String generatePropertyAccessor(final String name, final PropertyAccessor accessor) {
        final StringBuilder sb = new StringBuilder();

        final String accessorClass = getBoxedClassName(accessor.getClassName());
        final String accessorPropertyClass = getBoxedClassName(accessor.getPropertyClassName());

        final String accessing;
        final String mutating;
        if (accessor.getProperty().getAccessor() != null) {
            accessing = "    return o." + accessor.getProperty().getAccessor().getName() + "();\n";
        } else {
            accessing = "    return null;";
        }
        if (accessor.getProperty().getMutator() != null) {
            mutating = "    o." + accessor.getProperty().getMutator().getName() + "(value);\n";
        } else {
            mutating = "";
        }

        sb.append("private final static Property " + name + " = new Property<" + accessorClass
                + ", " + accessorPropertyClass + ">() {\n" + "  public String getName() {\n"
                + "    return \"" + accessor.getPropertyName() + "\";\n" + "  }\n" + "  public "
                + accessorPropertyClass + " getValue(final " + accessorClass + " o) {\n"
                + accessing + "  }\n" + "  public void setValue(final " + accessorClass
                + " o, final " + accessorPropertyClass + " value) {\n" + mutating + "  }\n"
                + "};\n");
        return sb.toString();
    }

    private String getPropertyAccessorName(final String className, final BeanProperty property) {
        final String propertyClassName = property.getType().getValue();
        final String propertyName = property.getName();

        final PropertyAccessor a = new PropertyAccessor(className, propertyClassName, propertyName,
                property);
        String name = propertyAccessors.get(a);
        if (name == null) {
            name = "p" + id++;
            propertyAccessors.put(a, name);
        }
        return name;
    }

    /**
     * Exposed for test purposes.
     */
    protected Map<PropertyAccessor, String> getPropertyAccessors() {
        return propertyAccessors;
    }

}
